import re
import socket
import signal

import test.utils
import util.dtest as dtest

#----------------------------------------------------------------------------
class NeonNode(dtest.Node):
    def __init__(self, name):
        dtest.Node.__init__(self, name)

        self.reg_std_packages()
        self.reg_interface('main')
        self.reg_tx()

        self.tx_log_target   = '%s.tx' % self.name()
        self.sync_log_target = '%s.sync' % self.name()
        self.neon_log_target = '%s.neon' % self.name()

        self.tx_transports_ = []

        self.tx_proc   = None
        self.sync_proc = None
        self.neon_proc = None

        # state variables
        self.state_ = dtest.AppState()

    def reg_std_packages(self):
        self.reg_py_package('io')
        self.reg_py_package('neon')
        self.reg_py_package('tx')
        self.reg_py_package('util')
        self.reg_php_package('php')
        self.reg_file('run-tx')
        self.reg_file('run-sync')
        self.reg_file('run-neon')

    def reg_tx(self):
        self.reg_port('tx-api-port')

    def reg_tx_tcp_out(self, sym_port, interval):
        self.tx_transports_.append(TxTcpOut(sym_port, interval))

    def reg_tx_tcp_in(self, name = None, interface = None):
        if name is None:
            name = 'tcp_in_%d' % len(self.tx_transports_)
        sym_port = self.reg_port(name, interface)
        self.tx_transports_.append(TxTcpIn(sym_port))
        return sym_port

    def tx_env(self, e = None):
        if e is None:
            e = {}
        else:
            e = e.copy()
            
        dtest.set_ifn(e, 'PYTHONPATH', '.')
        dtest.set_ifn(e, 'TX_ID', '%s' % self.name())
	dtest.set_ifn(e, 'TX_ADDR', '%s:%s' % 
		      (self.interfaces()['main'], self.ports()['tx-api-port']))
        dtest.set_ifn(e, 'TX_DTEST', '1')

        i = 0
        for t in self.tx_transports_:
            e['TX_TRANSPORT_%d' % i] = str(t)
            i = i + 1
        return e

    def sync_env(self, e = None):
        if not e:
            e = {}
        else:
            e = e.copy()
            
        dtest.set_ifn(e, 'PYTHONPATH', '.')
        dtest.set_ifn(e, 'NEON_ID', '%s' % self.name())
        dtest.set_ifn(e, 'NEON_TX_ADDR', "%s:%d" % (self.interfaces()['main'],
                                                    self.ports()['tx-api-port']))
        dtest.set_ifn(e, 'NEON_DB_TYPE', 'sqlite')
        dtest.set_ifn(e, 'NEON_DB_FILE', 'neon.db')
        dtest.set_ifn(e, 'NEON_TYPE_FILE', '')
        dtest.set_ifn(e, 'NEON_SYNC_INTERVAL', 1)
        return e

    def setup_logging(self):
        logger = self.dtest_.logger
        # logger.add_sink(FileLogSink('tx-%s' % self.name()))
        # logger.add_sink(FileLogSink('neon-%s' % self.name()))
        logger.add_sink(dtest.PrintLogSink(self.tx_log_target))
        logger.add_sink(dtest.PrintLogSink(self.sync_log_target))
        logger.add_sink(dtest.PrintLogSink(self.neon_log_target))
        
        logger.add_rule(self.tx_log_target)
        logger.add_rule(self.sync_log_target)
        logger.add_rule(self.neon_log_target)

    def state_watch_cb(self, path, data):
        """Handles the state for dtest:* state transition messages."""
        m = re.match(r'.*!dtest:([^ ]+)', data.strip())
        if m:
            self.state_.set(m.group(1))
            self.dtest_.check_state_change_cbs()

    @dtest.Node.test_method
    def pause_proc_(self, pause, proc):
	"""Start and stop process"""
	if pause: s = signal.SIGSTOP
	else: s = signal.SIGCONT
	self.host().signal(proc.pid(), s)

    @dtest.Node.test_method
    def run_tx(self, options = None, tx_opt_env = None):
        """Run the tx daemon asynchronously"""
        options = options or []
        tx_opt_env = tx_opt_env or {}

        options.extend(self.dtest_.run_options)

        cmd = ['run-tx']
        if 'debug' in options:
            cmd.append('--debug')

        ah = dtest.DefAsyncHandler(self.tx_log_target)
        ah.add_stdout_cb(self.state_watch_cb)
        ah.add_stderr_cb(self.state_watch_cb)
        
        h = self.dtest_.reg_handler(ah)
        self.host().run_cmd_async(cmd, h, self.tx_env(tx_opt_env))
        self.tx_proc = h

    def pause_tx(self, pause):
	self.pause_proc_(pause, self.tx_proc)

    @dtest.Node.test_method
    def run_sync(self, options = None, opt_env = None):
        """Run the sync daemon asynchronously"""
        options = options or []
        opt_env = opt_env or {}

        options.extend(self.dtest_.run_options)

        cmd = ['run-sync']
        if 'debug' in options:
            cmd.append('--debug')
            
        if 'initdb' in options:
            cmd.append('--init-db')

        ah = dtest.DefAsyncHandler(self.sync_log_target)
        ah.add_stdout_cb(self.state_watch_cb)
        ah.add_stderr_cb(self.state_watch_cb)

        h = self.dtest_.reg_handler(ah)
        self.host().run_cmd_async(cmd, h, self.sync_env(opt_env))
        self.sync_proc = h

    @dtest.Node.test_method
    def run_sync_init(self, options = None, opt_env = None):
        """Run the sync daemon init routine"""
        options = options or []
        opt_env = opt_env or {}

        options.extend(self.dtest_.run_options)

        cmd = ['./run-sync']
        if 'debug' in options:
            cmd.append('--debug')
            
        # XXX/bowei -- should be option?
        cmd.append('--init-db')
        cmd.append('--quit-after-init')

        self.host().run_cmd_with_env(opt_env, *cmd)

    def pause_sync(self, pause):
	self.pause_proc_(pause, self.sync_proc)

    def tx_api_sock(self):
        """Open a socket to communicate to tx directly (note: you
        shouldn't do this if you have Neon already connected.)"""
        sock = socket.socket()
        sock.connect((self.interfaces()['main'],
                      int(self.ports()['tx-api-port'])))
        return sock

#----------------------------------------------------------------------------
class TxTcpIn:
    def __init__(self, symport):
        self.symport = symport

    def __str__(self):
        return 'tcp_in,%s,%s' % self.symport.ap()

class TxTcpOut:
    def __init__(self, symport, interval):
        self.symport = symport
        self.interval = interval

    def __str__(self):
        ap = self.symport.ap()
        return 'tcp_out,%s,%s,%s' % (ap[0], ap[1], self.interval)

#----------------------------------------------------------------------------
class TxClientStateMachine:
    """State machine for interactive tests with tx. Subclass to
    customize functionality."""

    def __init__(self, sock):
        self.sock_ = sock
        self.quit_ = False
    
    def handshake_out(self, remote_id, proto):
        """@return data for the handshake"""
        raise NotImplemented()

    def handshake_in(self, remote_id, proto, hs):
        """Handshake received from tx"""
        raise NotImplemented()

    def data_out(self, remote_id, proto):
        """@return data to send out"""
        raise NotImplemented()

    def data_in(self, remote_id, data):
        """Deal with the data sent in"""
        raise NotImplemented()

    def connection_failure(self, info):
        """Tx signals connection failure"""
        raise NotImplemented()
    
    def run(self):
        while True:
            if self.quit_:
                break
            l = test.utils.sock_readln(self.sock_).strip()
            if l == '':
                break
            if l[0] == 'D':
                l = l.split(' ', 2)
                self.data_in(l[1], l[2])
            elif l[0] == 'h':
                l = l.split(' ', 2)
                self.sock_.send('h %s %s %s\n' %
                       (l[1], l[2], self.handshake_out(l[1], l[2])))
            elif l[0] == 'H':
                l = l.split(' ', 3)
                self.handshake_in(l[1], l[2], l[3])
            elif l[0] == 'f':
                l = l.split(' ', 1)
                self.connection_failure(l[1])
            elif l[0] == 'd':
                l = l.split(' ', 2)
                self.sock_.send('d %s %s %s\n' %
                       (l[1], l[2], self.data_out(l[1], l[2])))
            else:
                raise Exception('Got unknown message from tx: "%s"' % l)
            
    def send_quit(self):
        self.sock_.send('q')

#----------------------------------------------------------------------------
# Predicates and filters for convienience in checking the state
class Conditions:
    def __init__(self, dtest):
        self.dtest = dtest
        
    def filter_neon_node(self, nlist):
        return [n for n in nlist if isinstance(n, NeonNode)]

    @test.utils.curry_method
    def on_all(self, pred, filter = None):
        filter = filter or self.filter_neon_node
        nodes = filter(self.dtest.nodes.values())
        all = True
        for n in nodes:
            all = all and pred(n)
        return all

    def tx_is_running(self, n):
        return n.state_.get('tx', 'running') == '1'

    def tx_is_not_running(self, n):
        return n.tx_proc.status() is not None

    def sync_is_running(self, n):
        return n.state_.get('sync', 'running') == '1'

    def sync_is_not_running(self, n):
        return n.sync_proc.status() is not None

    def channel_state(self, node, proto, remote_id, state):
        n = self.dtest.nodes[node]
        c = n.state_.get('tx', 'channel', proto, remote_id)
        if c is None:
            return False
        return state == c
